using System;
using System.Collections.Generic;
using Indiefreaks.Xna.Core;
using Indiefreaks.Xna.Logic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
#if WINDOWS_PHONE
using Microsoft.Xna.Framework.Input.Touch;
#endif

namespace Indiefreaks.Xna.Input
{
    /// <summary>
    ///   The InputManager is responsible of updating player inputs and ensure it complies to XBLIG input compliancy
    /// </summary>
    /// <remarks>
    ///   <para>In XBLIG, in order to pass Peer Review, a game must ensure that when a GamePad is disconnected, the game is aware of it 
    ///     and can pause if there are no other devices connected. If a GamePad is connected during a game, it must also be notified and
    ///     allow the newly plugged device to play</para>
    ///   <para>InputServices handles just that through a set of Connection, Disconnection events and a LogicalPlayerIndex enumeration
    ///     you can use to know which PlayerIndex is in your game</para>
    /// </remarks>
    public class InputManager : GameComponent
    {
#if WINDOWS
        private KeyboardState _keyboardState;
        private MouseState _mouseState;
        private readonly KeyboardInputState _keyboard = new KeyboardInputState();
        private readonly MouseInputState _mouse = new MouseInputState();
        private Point _mousePrev;
        private bool _windowFocused = true, _mousePosSet;
        private int _focusSkip;
        private static bool _centreMouse, _mouseCentred, _centreMousePrevious;
        private static Point _mouseCentrePoint;
#elif WINDOWS_PHONE
        private readonly List<TouchLocation> _previousTouches = new List<TouchLocation>();
        private readonly List<TouchLocation> _currentTouches = new List<TouchLocation>();
#endif

        private readonly Dictionary<LogicalPlayerIndex, PlayerInput> _logicalPlayers;
        private readonly NoPlayerInput _noInputDeviceConnectedPlayer1;
        private readonly NoPlayerInput _noInputDeviceConnectedPlayer2;
        private readonly NoPlayerInput _noInputDeviceConnectedPlayer3;
        private readonly NoPlayerInput _noInputDeviceConnectedPlayer4;

        protected internal readonly PlayerInput Player1;
        protected internal readonly PlayerInput Player2;
        protected internal readonly PlayerInput Player3;
        protected internal readonly PlayerInput Player4;

        /// <summary>
        ///   Creates a new instance of the InputManager component and adds itself to the game's component collection as well as Service
        /// </summary>
        /// <param name = "application">The game instance</param>
        public InputManager(Application application)
            : base(application)
        {
            if (application == null)
                throw new ArgumentNullException("application", "InputManager requires a valid application instance");

            if (Application.Input != null)
                throw new CoreException("There already is an InputManager instance.");
            application.Components.Add(this);
            application.Services.AddService(typeof (InputManager), this);

            Player1 = new PlayerInput(PlayerIndex.One);
            Player1.Connected += OnPlayerConnected;
            Player1.Disconnected += OnPlayerDisconnected;

            Player2 = new PlayerInput(PlayerIndex.Two);
            Player2.Connected += OnPlayerConnected;
            Player2.Disconnected += OnPlayerDisconnected;

            Player3 = new PlayerInput(PlayerIndex.Three);
            Player3.Connected += OnPlayerConnected;
            Player3.Disconnected += OnPlayerDisconnected;

            Player4 = new PlayerInput(PlayerIndex.Four);
            Player4.Connected += OnPlayerConnected;
            Player4.Disconnected += OnPlayerDisconnected;

            _noInputDeviceConnectedPlayer1 = new NoPlayerInput(PlayerIndex.One);
            _noInputDeviceConnectedPlayer2 = new NoPlayerInput(PlayerIndex.Two);
            _noInputDeviceConnectedPlayer3 = new NoPlayerInput(PlayerIndex.Three);
            _noInputDeviceConnectedPlayer4 = new NoPlayerInput(PlayerIndex.Four);

            _logicalPlayers = new Dictionary<LogicalPlayerIndex, PlayerInput>();

            Player1.CheckConnectionStatus();
            Player2.CheckConnectionStatus();
            Player3.CheckConnectionStatus();
            Player4.CheckConnectionStatus();

#if WINDOWS
            Game.Activated += delegate { _windowFocused = true; };
            Game.Deactivated += delegate { _windowFocused = false; };
            _windowFocused = true;
#endif
        }

        /// <summary>
        ///   Returns true if at least one Input device is connected; false otherwise
        /// </summary>
        public bool IsInputDevicePlugged { get; private set; }

        /// <summary>
        ///   Returns the Logical Player 1 PlayerInput instance
        /// </summary>
        public PlayerInput PlayerOne
        {
            get { return !_logicalPlayers.ContainsKey(LogicalPlayerIndex.One) ? _noInputDeviceConnectedPlayer1 : _logicalPlayers[LogicalPlayerIndex.One]; }
        }

        /// <summary>
        ///   Returns the Logical Player 2 PlayerInput instance
        /// </summary>
        public PlayerInput PlayerTwo
        {
            get { return !_logicalPlayers.ContainsKey(LogicalPlayerIndex.Two) ? _noInputDeviceConnectedPlayer2 : _logicalPlayers[LogicalPlayerIndex.Two]; }
        }


        /// <summary>
        ///   Returns the Logical Player 3 PlayerInput instance
        /// </summary>
        public PlayerInput PlayerThree
        {
            get { return !_logicalPlayers.ContainsKey(LogicalPlayerIndex.Three) ? _noInputDeviceConnectedPlayer3 : _logicalPlayers[LogicalPlayerIndex.Three]; }
        }

        /// <summary>
        ///   Returns the Logical Player 4 PlayerInput instance
        /// </summary>
        public PlayerInput PlayerFour
        {
            get { return !_logicalPlayers.ContainsKey(LogicalPlayerIndex.Four) ? _noInputDeviceConnectedPlayer4 : _logicalPlayers[LogicalPlayerIndex.Four]; }
        }

        public PlayerInput GetPlayerInput(PlayerIndex playerIndex)
        {
            switch(playerIndex)
            {
                case PlayerIndex.Four:
                    return Player4;
                case PlayerIndex.Three:
                    return Player3;
                case PlayerIndex.Two:
                    return Player2;
                case PlayerIndex.One:
                default:
                    return Player1;
            }
        }
        
#if WINDOWS
        public KeyboardState KeyboardState
        {
            get { return _keyboard; }
        }

        public bool IsMouseVisible
        {
            get { return Game.IsMouseVisible; }
            set { Game.IsMouseVisible = value; }
        }

        public bool UseKeyboardAndMouse { get; set; }

        internal bool DesiredMouseCentered { get; set; }

        internal bool MouseCentred
        {
            get { return _mouseCentred && _windowFocused; }
        }

        internal Point MouseCentredPosition
        {
            get { return _mouseCentrePoint; }
        }

        internal bool WindowFocused
        {
            get { return _windowFocused; }
        }

        internal Point MousePreviousPosition
        {
            get { return _mousePrev; }
        }

        /// <summary>
        ///   [Windows Only]
        ///   Using <see cref = "PlayerInput" /> is recommended over direct state access
        /// </summary>
        public MouseState MouseState
        {
            get { return _mouse; }
        }
#endif

        /// <summary>
        ///   Subscribe to this event if you are looking to get noticed when no Input devices are connected
        /// </summary>
        /// <remarks>
        ///   Useful if you want to pause the game until an input device gets connected. You can then test for InputManager.IsInputDeviceConnected
        ///   property every other frame (i.e. in the Update call) to see if at least one input device is connected
        /// </remarks>
        public event EventHandler AllInputDevicesDisconnected;

        /// <summary>
        ///   Internal update
        /// </summary>
        /// <param name = "tick"></param>
        internal void Update(long tick)
        {
            Player1.Update(tick);
            Player2.Update(tick);
            Player3.Update(tick);
            Player4.Update(tick);
        }

        /// <summary>
        ///   Called when an input device gets connected to the machine.
        /// </summary>
        private void OnPlayerConnected(object sender, EventArgs e)
        {
            var playerInput = (PlayerInput) sender;

            LogicalPlayerIndex nextLogicalPlayerIndex;
            if (!_logicalPlayers.ContainsKey(LogicalPlayerIndex.One))
                nextLogicalPlayerIndex = LogicalPlayerIndex.One;
            else if (!_logicalPlayers.ContainsKey(LogicalPlayerIndex.Two))
                nextLogicalPlayerIndex = LogicalPlayerIndex.Two;
            else if (!_logicalPlayers.ContainsKey(LogicalPlayerIndex.Three))
                nextLogicalPlayerIndex = LogicalPlayerIndex.Three;
            else
                nextLogicalPlayerIndex = LogicalPlayerIndex.Four;

            _logicalPlayers.Add(nextLogicalPlayerIndex, playerInput);
            playerInput.LogicalPlayerIndex = nextLogicalPlayerIndex;

            IsInputDevicePlugged = _logicalPlayers.Count > 0;
        }

        /// <summary>
        ///   Called when an input device gets disconnected to the machine.
        /// </summary>
        private void OnPlayerDisconnected(object sender, EventArgs e)
        {
            var playerInput = (PlayerInput) sender;

            _logicalPlayers.Remove(playerInput.LogicalPlayerIndex);
            playerInput.LogicalPlayerIndex = LogicalPlayerIndex.None;

            IsInputDevicePlugged = _logicalPlayers.Count > 0;

            if (!IsInputDevicePlugged)
                OnAllInputDevicesDisconnected();
        }

        /// <summary>
        ///   Called when no input devices are connected to the machine.
        /// </summary>
        private void OnAllInputDevicesDisconnected()
        {
            if (AllInputDevicesDisconnected != null)
                AllInputDevicesDisconnected(this, EventArgs.Empty);
        }

        /// <summary>
        ///   Updates the object and its contained resources.
        /// </summary>
        /// <param name = "gameTime" />
        public override void Update(GameTime gameTime)
        {
            long ticks = gameTime.TotalGameTime.Ticks;
#if WINDOWS
            UpdateWindowsInput(ticks);
#elif WINDOWS_PHONE
            UpdateWindowsTouchInput(ticks);
#endif
            Update(ticks);
        }

#if WINDOWS
        private void UpdateWindowsInput(long tick)
        {
            _mousePrev = new Point(_mouse.X, _mouse.Y);

            _keyboardState = Keyboard.GetState();
            _mouseState = Mouse.GetState();

            var p = new Point(Game.Window.ClientBounds.Width/2, Game.Window.ClientBounds.Height/2);

            if (_centreMouse && _windowFocused)
                Mouse.SetPosition(p.X, p.Y);

            if (_centreMousePrevious)
            {
                _mouseCentred = true;
                _mouseCentrePoint = p;
            }

            _centreMousePrevious = _centreMouse;

            if (!_windowFocused)
                _focusSkip = 5;
            if (!_mousePosSet || _focusSkip > 0)
            {
                _focusSkip--;
                _mousePrev = new Point(_mouse.X, _mouse.Y);
                _mouseCentrePoint = _mousePrev;
                _mousePosSet = true;
            }

            _centreMouse = DesiredMouseCentered && WindowFocused;

            _keyboard.Update(tick, ref _keyboardState);
            _mouse.Update(tick, ref _mouseState);

            Player1.SetKms(this);
            Player2.SetKms(this);
            Player3.SetKms(this);
            Player4.SetKms(this);
        }
#endif

#if WINDOWS_PHONE
        private void UpdateWindowsTouchInput(long tick)
        {
            _previousTouches.AddRange(_currentTouches);
            _currentTouches.Clear();

            var touches = TouchPanel.GetState();
            foreach (var touche in touches)
            {
                _currentTouches.Add(touche);
            }
        }
#endif
    }
}